package ch.zhaw.cctd.domain.match;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import ch.zhaw.cctd.domain.GraphicalElement;
import ch.zhaw.cctd.domain.Movable;
import ch.zhaw.cctd.domain.Shootable;
import ch.zhaw.cctd.domain.Shooter;
import ch.zhaw.cctd.domain.creep.Creep;
import ch.zhaw.cctd.domain.handlers.GameThread;
import ch.zhaw.cctd.domain.map.Map;
import ch.zhaw.cctd.domain.map.MapElement;
import ch.zhaw.cctd.domain.player.Player;
import ch.zhaw.cctd.domain.player.RessourceException;
import ch.zhaw.cctd.domain.player.SpawnPoint;
import ch.zhaw.cctd.domain.prototype.CreepPrototype;
import ch.zhaw.cctd.domain.prototype.MapPrototype;
import ch.zhaw.cctd.domain.prototype.Prototype;
import ch.zhaw.cctd.domain.prototype.PrototypeList;
import ch.zhaw.cctd.domain.prototype.TowerPrototype;
import ch.zhaw.cctd.domain.prototype.WavePrototype;
import ch.zhaw.cctd.domain.shot.Shot;
import ch.zhaw.cctd.domain.tower.Tower;
import ch.zhaw.cctd.domain.tower.TowerBuildException;
import ch.zhaw.cctd.logic.event.EventHandler;
import ch.zhaw.cctd.logic.network.RemoteClient;
import ch.zhaw.cctd.logic.network.RemoteServer;
import ch.zhaw.cctd.logic.xml.XmlConfigReader;
import ch.zhaw.cctd.logic.xml.XmlReadException;

/**
 * Zuständig für den Zustand des Multiplayer Spiels. Hält alle Worker Threads
 * die den Spielstatus ständig neu berechnen. Und hält die aktuell laufende Map.
 * 
 * @author Rolf Koch
 * @version 1.0
 */
public class MatchServer extends UnicastRemoteObject implements RemoteServer, Serializable {

	/**
	 * Serializable ID
	 */
	private static final long serialVersionUID = 2420188720762229872L;

	/**
	 * Logger
	 */
	private static final Logger logger = LoggerFactory.getLogger(MatchServer.class);

	// Player List
	private List<Player> playerSet;
	// Map
	private Map map;
	// Prototypes
	private PrototypeList prototypes;

	// Eventhandler
	private EventHandler eventhandler;

	// Thread
	private GameThread game;

	// Client List
	private ClientList<RemoteClient> clientList;

	// Win/Lose
	boolean lose;
	boolean win;

	/**
	 * Default Constructor which loads the XML File
	 * 
	 * @param eventhandler
	 * @param stream
	 * @throws RemoteException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 */
	public MatchServer(EventHandler eventhandler, InputStream stream) throws XmlReadException, RemoteException {
		this(eventhandler, XmlConfigReader.readData(stream));
	}

	/**
	 * Constructor with prototypes already loaded
	 * 
	 * @param eventhandler
	 * @param prototypes
	 * @throws RemoteException
	 */

	public MatchServer(EventHandler eventhandler, PrototypeList prototypes) throws RemoteException {
		super();
		this.eventhandler = eventhandler;
		this.prototypes = prototypes;
		playerSet = new ArrayList<Player>();
		clientList = new ClientList<RemoteClient>();
		logger.trace("Match create");
		lose = false;
		win = false;
	}

	/**
	 * Returns all Prototypes
	 * 
	 * @return prototypes
	 */
	@Override
	public PrototypeList getPrototypeList() throws RemoteException {
		return this.prototypes;
	}

	/**
	 * Returns a specific Prototype. The Prototype has to be defined by his Type
	 * and ID.
	 * 
	 * @param <T>
	 * @param c
	 * @param id
	 * @return Prototype vom entsprechenden Typ
	 */
	public <T extends Prototype> T getPrototype(Class<T> c, String id) throws RemoteException {
		return this.prototypes.getPrototype(c, id);
	}

	/**
	 * Returns the EventHandler
	 * 
	 * @return
	 */
	@Override
	public EventHandler getEventHandler() throws RemoteException {
		return this.eventhandler;
	}

	/**
	 * Initializes the Match and runs all Threads.
	 */
	@Override
	public void startMatch() throws RemoteException {
		logger.debug("Match Starting");

		// Setup the Map
		setMap(new Map(this));
		List<MapPrototype> mapPrototypeList = this.getPrototypeList().getPrototypes(MapPrototype.class);
		map.loadMapProperties(mapPrototypeList.get(0));

		// Setup the GameThread
		logger.trace("Game Thread start");
		game = new GameThread(this);
		game.init(this.prototypes.getPrototypes(WavePrototype.class));
		// Start the Shooterhandler
		Thread gameThread = new Thread(game);
		gameThread.setName("Game Thread");
		gameThread.start();
		logger.trace("Game Thread running");

		
		logger.debug("Match start Finished");
	}

	/**
	 * Add a Player to the Match
	 * 
	 * @param players
	 */
	@Override
	public void addPlayer(Player player) throws RemoteException {
		logger.trace("Adding Player {}", player.getName());
		for(Player p : playerSet) {
			if(p.getPlayerId() == player.getPlayerId()) {
				throw new IllegalArgumentException("Player with this ID is allready in the game");
			}
			else if (player.getName().equalsIgnoreCase(p.getName())) {
				throw new IllegalArgumentException("Player with name "+player.getName()+" already exists");
			}
		}
		playerSet.add(player);
	}

	/**
	 * Add Multiple Players
	 * 
	 * @param players
	 * @throws RemoteException
	 */
	public void addPlayers(Player... players) throws RemoteException {
		for (Player p : players) {
			addPlayer(p);
		}
	}


	/**
	 * Destroys all Objects, Stops the Threads and Terminates the Game
	 */
	public void stopMatch() throws RemoteException {
		logger.trace("Stopping Game Thread");
		if(game!= null && game.isRunning()) {
			game.stop();
		
			while(game.isRunning()) {
				//Endless Waiting
			}
		}
		logger.trace(".. stopped");
	}

	/**
	 * For Testing Only
	 * 
	 * @param map
	 */
	public void setMap(Map map) throws RemoteException {
		logger.warn("Custom Map added");
		this.map = map;
	}

	/**
	 * Tells the game that it has been Won
	 * 
	 */
	@Override
	synchronized public void win() throws RemoteException {
		// The game has been Won
		if (!win && !lose) {
			stopMatch();
			logger.info("Game won");
			win = true;
		}
	}

	/**
	 * Tells the game that it has been Won
	 * 
	 */
	@Override
	synchronized public void lose() throws RemoteException {
		// The game has been lost
		if (!lose && !win) {

			stopMatch();
			logger.info("Game lost");
			lose = true;
		}
	}

	/**
	 * Returns a Player by ID
	 * 
	 * @param playerId
	 * @return player
	 */
	@Override
	public Player getPlayer(int playerId) throws RemoteException {
		for (Player p : playerSet) {
			if (p.getPlayerId() == playerId)
				return p;
		}
		return null;
	}

	/**
	 * Get PlayerCount
	 * 
	 * @return Playercount
	 */
	@Override
	public int getPlayerCount() throws RemoteException {
		return playerSet.size();
	}

	/**
	 * Get Tower List
	 * 
	 * @return TowerList
	 */
	@Override
	public List<Tower> getTowerList() throws RemoteException {
		return map.getTowerList();
	}

	/**
	 * Get Creep List
	 * 
	 * @return CreepList
	 */
	@Override
	public List<Creep> getCreepList() throws RemoteException {
		return map.getCreepList();
	}

	/**
	 * Get Shot List
	 * 
	 * @return ShotList
	 */
	@Override
	public List<Shot> getShotList() throws RemoteException {
		return map.getShotList();
	}


	/**
	 * Returns all Static Map  Objects as Graphical Element List from the Server
	 */
	public List<? extends GraphicalElement> getStaticMapObjects() throws RemoteException {

		return map.getStaticMapObjects();
	}
	
	/**
	 * Returns all Tower  Objects as Graphical Element List from the Server
	 */
	public List<? extends GraphicalElement> getTowersMapObjects() throws RemoteException {

		return map.getTowersMapObjects();
	}
	
	/**
	 * Returns all Movable (Creep + Shots) Objects as Graphical Element List from the Server
	 */
	public List<? extends GraphicalElement> getMovableMapObjects() throws RemoteException {

		return map.getMovableMapObjects();
	}
	
	/**
	 * Spawn a Creep for each Player in the Game
	 * 
	 * @param creepPrototype
	 */
	public void spawnCreep(CreepPrototype creepPrototype) throws RemoteException {
		logger.trace("start spawning creeps for {}  Players ", playerSet.size());
		for (Player p : playerSet) {
			logger.trace("Spawning a Creep for Player {} ", p.getPlayerId());
			logger.trace("player has  {} SpawnPoint ", p.getSpawnPointList().size());
			for (SpawnPoint sp : p.getSpawnPointList()) {
				map.spawnCreep(creepPrototype, sp);
			}
		}
	}

	/**
	 * Build a new Tower
	 * 
	 * @param towerPrototype
	 * @param point
	 * @param owner
	 */
	@Override
	public void buildTower(TowerPrototype towerPrototype, Point point, int ownerId) throws RemoteException, RessourceException, TowerBuildException {
		try {
			map.buildTower(point, towerPrototype, getPlayer(ownerId));
		} catch (RessourceException e) {
			logger.info("Not enough Ressources");
			throw e;
		} catch (TowerBuildException e) {
			throw e;
		}
	}

	/**
	 * Returns the Map Element a Player has clicked on by Point
	 * 
	 * @param point
	 * @return MapElement
	 */
	@Override
	public MapElement getMapObject(Point p) throws RemoteException {
		MapElement mapElement = null;
		// Check if its any Creep
		for (Creep c : getCreepList()) {
			if (c.contains(p))
				mapElement = c;
		}

		// Check if its any Tower
		for (Tower t : getTowerList()) {
			if (t.contains(p))
				mapElement = t;
		}

		return mapElement;
	}

	/**
	 * Transfer Ressources
	 * 
	 * @param source
	 * @param target
	 * @param amount
	 */
	@Override
	public void ressourceTransfer(int source, int target, int amount) throws RemoteException {
		getPlayer(target).receiveRessource(getPlayer(source).payRessource(amount));
	}

	/**
	 * Register a new RemoteClient and return the Client ID which is also the
	 * Player ID
	 * 
	 * @param remoteclient
	 * @return ClientID
	 */
	@Override
	public int registerClient(RemoteClient rc) throws RemoteException, IllegalStateException {
		if(!isStarted()) {
			int clientId = clientList.getFirstFreeIndex();
			clientList.add(clientId, rc);
	
			logger.debug("Client Registered {}", clientId);
			return clientId;
		} else {
			throw new IllegalStateException("Game already running");
		}
	}

	/**
	 * Ping all Clients
	 */
	@Override
	public void checkClients() throws RemoteException {
		for (RemoteClient rc : clientList) {
			if (rc.ping()) {
				logger.trace("Client {} is here!", clientList.indexOf(rc));
			} else {
				unregisterClient(rc);
				logger.trace("Client {} is offline!", clientList.indexOf(rc));
			}
		}
	}

	/**
	 * Unregister a Client from the ClientList
	 */
	@Override
	public void unregisterClient(RemoteClient rc) throws RemoteException {
		int index = clientList.indexOf(rc);
		if(index>=0) {
			clientList.set(index, null);
			playerSet.remove(index);
		
			logger.debug("Unregistered Client  {} !", index);
		}
		if(clientList.getClientCount() == 0) {
			logger.trace("Last Player has left the Game, Stopping Match");
			stopMatch();
		}
	}

	/**
	 * Returns the MapSize as Dimension
	 * 
	 * @return dimension
	 */
	@Override
	public Dimension getMapSize() throws RemoteException {
		return map.getMapSize();
	}

	/**
	 * Checks if the Match is started and returns the result
	 * 
	 * @return boolean
	 */
	@Override
	public boolean isStarted() throws RemoteException {
		return !(map == null);
	}

	/**
	 * Get the Player Map with ID + Playername
	 * @return HashMap
	 */
	@Override
	public java.util.Map<Integer, String> getPlayerMap() throws RemoteException {
		logger.trace("playerSet size: {}", playerSet.size());
		java.util.Map<Integer, String> output = new HashMap<Integer, String>();
		for (Player p : playerSet) {
			if(p != null) {
				logger.trace("Player Name: {} at location: {}", p.getName(), p.getPlayerId() );
				output.put(p.getPlayerId(), p.getName());
			}
		}
		logger.trace("getPlayerMap size: {}", output.size());
		return output;
	}

	/**
	 * Returns the current Wave Number
	 * @return int
	 */
	@Override
	public int getWaveNumber() throws RemoteException {
		return game.getWaveCount();
	}

	/**
	 * Returns the Players Money
	 * @return int
	 */
	@Override
	public int getMoneyByPlayer(int playerId) throws RemoteException {
		for(Player p : playerSet) {
			if(p.getPlayerId() == playerId) {
				return p.getRessource();
			}
		}
		return 0;
	}

	/**
	 * Returns the current Creep count on the Map
	 * @return int
	 */
	@Override
	public int getCreepCount() throws RemoteException {
		return map.getCreepCount();
	}

	/**
	 * Checks if a Position is a valid Tower Location for the playerID
	 * @param Point Position auf der Map
	 * @param Player LocalPlayer
	 * @return boolean
	 */
	@Override
	public Point isValidTowerBuildLocation(Rectangle rect, int playerId) throws RemoteException {	
		return map.isGridLocationValid(rect, getPlayer(playerId));
	}

	@Override
	public int getPlayerMoney(int playerId) throws RemoteException {
		return getPlayer(playerId).getRessource();
	}

	public List<? extends Shootable> getShootableList() {
		return map.getShootableList();
	}

	public List<? extends Shooter> getShooterList() {
		return map.getShooterList();
	}

	public List<? extends Movable> getMovableList() {
		return map.getMovableList();
	}

	public void updateMovableLists() {
		map.updateMovableLists();
		
	}

}
